#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/fb.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/ioport.h>
#include <linux/dma-mapping.h>
#include <linux/clk.h>
#include <linux/console.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#include <asm/mach-types.h>

#if 0
#define DMA_ALLOC	dma_alloc_coherent
#define DMA_FREE	dma_free_coherent
#else
#define DMA_ALLOC	dma_alloc_writecombine
#define DMA_FREE	dma_free_writecombine
#endif


static int xres = 320;
static int yres = 240;
static int bpp = 16;
static int buffers = 2;
module_param(xres, int, 0664);
module_param(yres, int, 0664);
module_param(bpp, int, 0664);
module_param(buffers, int, 0664);
MODULE_PARM_DESC(xres, "   xres for framebuffer(default is 320).");
MODULE_PARM_DESC(yres, "   yres for framebuffer(default is 240).");
MODULE_PARM_DESC(bpp, "    bpp for framebuffer(default is 16).");
MODULE_PARM_DESC(buffers, "buffers for framebuffer(default is 2).");

static struct fb_info *g_fbinfo;

static int vfb_map_video_memory(struct fb_info *fbi);
static int vfb_unmap_video_memory(struct fb_info *fbi);

/*
 * Set fixed framebuffer parameters based on variable settings.
 *
 * @param       info     framebuffer information pointer
 */
static int vfb_set_fix(struct fb_info *info)
{
	struct fb_fix_screeninfo *fix = &info->fix;
	struct fb_var_screeninfo *var = &info->var;

	fix->line_length = var->xres_virtual * var->bits_per_pixel / 8;

	fix->type = FB_TYPE_PACKED_PIXELS;
	fix->accel = FB_ACCEL_NONE;
	fix->visual = FB_VISUAL_TRUECOLOR;
	fix->xpanstep = 1;
	fix->ywrapstep = 1;
	fix->ypanstep = 1;
	return 0;
}


/*
 * Set framebuffer parameters and change the operating mode.
 *
 * @param       info     framebuffer information pointer
 */
static int vfb_set_par(struct fb_info *fbi)
{
	int retval = 0;
	u32 mem_len;

	dev_dbg(fbi->device, "Reconfiguring framebuffer\n");
	vfb_set_fix(fbi);

	mem_len = fbi->var.yres_virtual * fbi->fix.line_length;
	if (!fbi->fix.smem_start || (mem_len > fbi->fix.smem_len)) {
		if (fbi->fix.smem_start)
			vfb_unmap_video_memory(fbi);

		return vfb_map_video_memory(fbi);
	}
	return retval;
}


/*
 * Check framebuffer variable parameters and adjust to valid values.
 *
 * @param       var      framebuffer variable parameters
 *
 * @param       info     framebuffer information pointer
 */
static int vfb_check_var(struct fb_var_screeninfo *var, struct fb_info *info)
{
	/* fg should not bigger than bg */

	if (var->xres_virtual < var->xres)
		var->xres_virtual = var->xres;
	if (var->yres_virtual < var->yres)
		var->yres_virtual = var->yres;

	if ((var->bits_per_pixel != 32) && (var->bits_per_pixel != 24) &&
	    (var->bits_per_pixel != 16) && (var->bits_per_pixel != 12) &&
	    (var->bits_per_pixel != 8))
		var->bits_per_pixel = 16;

	switch (var->bits_per_pixel) {
	case 8:
		var->red.length = 3;
		var->red.offset = 5;
		var->red.msb_right = 0;

		var->green.length = 3;
		var->green.offset = 2;
		var->green.msb_right = 0;

		var->blue.length = 2;
		var->blue.offset = 0;
		var->blue.msb_right = 0;

		var->transp.length = 0;
		var->transp.offset = 0;
		var->transp.msb_right = 0;
		break;
	case 16:
		var->red.length = 5;
		var->red.offset = 11;
		var->red.msb_right = 0;

		var->green.length = 6;
		var->green.offset = 5;
		var->green.msb_right = 0;

		var->blue.length = 5;
		var->blue.offset = 0;
		var->blue.msb_right = 0;

		var->transp.length = 0;
		var->transp.offset = 0;
		var->transp.msb_right = 0;
		break;
	case 24:
		var->red.length = 8;
		var->red.offset = 16;
		var->red.msb_right = 0;

		var->green.length = 8;
		var->green.offset = 8;
		var->green.msb_right = 0;

		var->blue.length = 8;
		var->blue.offset = 0;
		var->blue.msb_right = 0;

		var->transp.length = 0;
		var->transp.offset = 0;
		var->transp.msb_right = 0;
		break;
	case 32:
		var->red.length = 8;
		var->red.offset = 16;
		var->red.msb_right = 0;

		var->green.length = 8;
		var->green.offset = 8;
		var->green.msb_right = 0;

		var->blue.length = 8;
		var->blue.offset = 0;
		var->blue.msb_right = 0;

		var->transp.length = 8;
		var->transp.offset = 24;
		var->transp.msb_right = 0;
		break;
	}

	var->height = -1;
	var->width = -1;
	var->grayscale = 0;
	return 0;
}

/*
 * Pan or Wrap the Display
 *
 * This call looks only at xoffset, yoffset and the FB_VMODE_YWRAP flag
 *
 * @param               var     Variable screen buffer information
 * @param               info    Framebuffer information pointer
 */
static int vfb_pan_display(struct fb_var_screeninfo *var, struct fb_info *info)
{
	if (info->var.yoffset == var->yoffset)
		return 0;	/* No change, do nothing */

	if ((var->yoffset + info->var.yres) > info->var.yres_virtual)
		return -EINVAL;

	info->var.yoffset = var->yoffset;
	return 0;
}

/*
 * Function to handle custom mmap for virtual framebuffer.
 *
 * @param       fbi     framebuffer information pointer
 *
 * @param       vma     Pointer to vm_area_struct
 */
static int vfb_mmap(struct fb_info *fbi, struct vm_area_struct *vma)
{
	u32 len;
	unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

	if (offset < fbi->fix.smem_len) {
		/* mapping framebuffer memory */
		len = fbi->fix.smem_len - offset;
		vma->vm_pgoff = (fbi->fix.smem_start + offset) >> PAGE_SHIFT;
	} else {
		return -EINVAL;
	}

	len = PAGE_ALIGN(len);
	if (vma->vm_end - vma->vm_start > len)
		return -EINVAL;

	/* make buffers bufferable */
	vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);

#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 7, 0)
	vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
#else
	vma->vm_flags |= VM_IO | VM_RESERVED;
#endif

	if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
			    vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
		dev_dbg(fbi->device, "mmap remap_pfn_range failed\n");
		return -ENOBUFS;
	}

	return 0;
}

/*!
 * This structure contains the pointers to the control functions that are
 * invoked by the core framebuffer driver to perform operations like
 * blitting, rectangle filling, copy regions and cursor definition.
 */
static struct fb_ops vfb_ops = {
	.owner = THIS_MODULE,
	.fb_set_par = vfb_set_par,
	.fb_check_var = vfb_check_var,
	.fb_pan_display = vfb_pan_display,
	.fb_mmap = vfb_mmap,
};


/*
 * Main framebuffer functions
 */

/*!
 * Allocates the DRAM memory for the frame buffer.      This buffer is remapped
 * into a non-cached, non-buffered, memory region to allow palette and pixel
 * writes to occur without flushing the cache.  Once this area is remapped,
 * all virtual memory access to the video memory should occur at the new region.
 *
 * @param       fbi     framebuffer information pointer
 *
 * @return      Error code indicating success or failure
 */
static int vfb_map_video_memory(struct fb_info *fbi)
{
	if (fbi->fix.smem_len < fbi->var.yres_virtual * fbi->fix.line_length)
		fbi->fix.smem_len = fbi->var.yres_virtual * fbi->fix.line_length;

	fbi->screen_base = DMA_ALLOC(0, fbi->fix.smem_len,
			(dma_addr_t *)&fbi->fix.smem_start, GFP_DMA | GFP_KERNEL);
	if (fbi->screen_base == 0) {
		pr_err("Unable to allocate fbmem len=%d\n", fbi->fix.smem_len);
		fbi->fix.smem_len = 0;
		fbi->fix.smem_start = 0;
		return -ENOBUFS;
	}

	pr_info("allocated fb @ paddr=0x%08X, size=%d.\n",
		(uint32_t) fbi->fix.smem_start, fbi->fix.smem_len);

	fbi->screen_size = fbi->fix.smem_len;

	/* Clear the screen */
	memset((char *)fbi->screen_base, 0, fbi->fix.smem_len);
	return 0;
}

/*!
 * De-allocates the DRAM memory for the frame buffer.
 *
 * @param       fbi     framebuffer information pointer
 *
 * @return      Error code indicating success or failure
 */
static int vfb_unmap_video_memory(struct fb_info *fbi)
{
	DMA_FREE(fbi->device, fbi->fix.smem_len,
		fbi->screen_base, fbi->fix.smem_start);
	fbi->screen_base = 0;
	fbi->fix.smem_start = 0;
	fbi->fix.smem_len = 0;
	return 0;
}

/*!
 * Initializes the framebuffer information pointer. After allocating
 * sufficient memory for the framebuffer structure, the fields are
 * filled with custom information passed in from the configurable
 * structures.  This includes information such as bits per pixel,
 * color maps, screen width/height and RGBA offsets.
 *
 * @return      Framebuffer structure initialized with our information
 */
static struct fb_info *vfb_init_fbinfo(struct fb_ops *ops)
{
	struct fb_info *fbi;

	/*
	 * Allocate sufficient memory for the fb structure
	 */
	fbi = framebuffer_alloc(sizeof(unsigned int), NULL);
	if (!fbi)
		return NULL;

	fbi->var.activate = FB_ACTIVATE_NOW;
	fbi->fbops = ops;
	fbi->flags = FBINFO_FLAG_DEFAULT;
	return fbi;
}


static int vfb_register(struct fb_info *fbi, unsigned int id)
{
	struct fb_videomode m;
	int ret = 0;

	strcpy(fbi->fix.id, "VFB");

	//Setup small default resolution
	fbi->var.xres = xres;
	fbi->var.yres = yres;
	fbi->var.xres_virtual = xres;
	fbi->var.yres_virtual = yres * buffers;
	fbi->var.xoffset = 0;
	fbi->var.yoffset = 0;
	fbi->var.bits_per_pixel = bpp;

	vfb_check_var(&fbi->var, fbi);
	vfb_set_fix(fbi);

	/*added first mode to fbi modelist*/
	if (!fbi->modelist.next || !fbi->modelist.prev)
		INIT_LIST_HEAD(&fbi->modelist);
	fb_var_to_videomode(&m, &fbi->var);
	fb_add_videomode(&m, &fbi->modelist);

	fbi->var.activate |= FB_ACTIVATE_FORCE;
	console_lock();
	fbi->flags |= FBINFO_MISC_USEREVENT;
	ret = fb_set_var(fbi, &fbi->var);
	fbi->flags &= ~FBINFO_MISC_USEREVENT;
	console_unlock();

	return register_framebuffer(fbi);
}

static void vfb_unregister(struct fb_info *fbi)
{
	unregister_framebuffer(fbi);
}

/*!
 * Main entry function for the framebuffer. The function registers the power
 * management callback functions with the kernel and also registers the MXCFB
 * callback functions with the core Linux framebuffer driver \b fbmem.c
 *
 * @return      Error code indicating success or failure
 */
int __init vfb_init(void)
{
	u32 *fbNum;
	int ret = 0;

	g_fbinfo = vfb_init_fbinfo(&vfb_ops);
	if (!g_fbinfo) {
		ret = -ENOMEM;
		goto err;
	}

	fbNum = (u32*)g_fbinfo->par;
	*fbNum = 0;

	ret = vfb_register(g_fbinfo, 0);
	if (ret < 0)
		goto err;

	pr_info("Register %s [%dx%d]bpp=%d.\n", g_fbinfo->fix.id,
		g_fbinfo->var.xres, g_fbinfo->var.yres, g_fbinfo->var.bits_per_pixel);
	return 0;

err:
	if(g_fbinfo) {
		vfb_unregister(g_fbinfo);
		framebuffer_release(g_fbinfo);
	}
	return ret;
}

void vfb_exit(void)
{
	if(g_fbinfo) {
		vfb_unregister(g_fbinfo);
		vfb_unmap_video_memory(g_fbinfo);
		framebuffer_release(g_fbinfo);
	}
}

module_init(vfb_init);
module_exit(vfb_exit);

MODULE_AUTHOR("Lenver Shan <lenver520@163.com>");
MODULE_DESCRIPTION("Virtual framebuffer driver");
MODULE_LICENSE("GPL");
MODULE_SUPPORTED_DEVICE("fb");
